// The NSpring Framework for .NET
// Copyright (c) 2003, Jeffrey Varszegi
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without modification, 
// are permitted provided that the following conditions are met:
// 
// - Redistributions of source code must retain the above copyright notice, this 
// list of conditions and the following disclaimer.
// 
// - Redistributions in binary form must reproduce the above copyright notice, this 
// list of conditions and the following disclaimer in the documentation and/or other 
// materials provided with the distribution.
// 
// - Neither the name of the NSpring project nor the names of its contributors may 
// be used to endorse or promote products derived from this software without 
// specific prior written permission from the copyright owner.
// 
// - No product derived from this software may be called "NSpring", nor may 
// "NSpring" appear in the name of such a product, without specific prior written 
// permission from the copyright owner.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
// IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
// OF THE POSSIBILITY OF SUCH DAMAGE. 

namespace NSpring.Logging.Common.Pattern {

using System;
using System.Collections;

/// <summary>
/// <para>
/// A parser that generates Pattern instances from string input.
/// All underlying pattern-related code is subject to change without warning
/// or deprecation, and should not be called directly by application code
/// </para>
/// </summary>
internal sealed class PatternParser {
    private static TextToken EMPTY_TEXT_TOKEN           = new TextToken("");

    private static char[] EMPTY_CHAR_ARRAY              = {};
    private const string BLANK_STRING                   = "";
    private const char COLON_CHAR                       = ':';
    private const char OPEN_PARENTHESIS_CHAR            = '(';
    private const char CLOSE_PARENTHESIS_CHAR           = ')';
    private const char OPEN_CURLY_BRACE_CHAR            = '{';
    private const char CLOSE_CURLY_BRACE_CHAR           = '}';
    private const char EQUALS_SIGN_CHAR                 = '=';
    private static char[] FORMATTING_HINT_SEPARATORS    = {';', ','};

    private static string NEW_LINE                      = new string(new char[] {(char)13, (char)10});
    private static string NEW_LINE_CHAR                 = "\n";
    
    private static TokenInfo[] tokenInfoArray           = {
        new ContextPropertyTokenInfo("applicationname", "an", ContextPropertyType.ApplicationName),
        new ContextPropertyTokenInfo("systemname", "sn", ContextPropertyType.SystemName),
        new ContextPropertyTokenInfo("username", "un", ContextPropertyType.UserName),
        new ContextPropertyTokenInfo("threadname", "tn", ContextPropertyType.ThreadName),
        new ContextPropertyTokenInfo("threadpriority", "tp", ContextPropertyType.ThreadPriority),
        new ContextPropertyTokenInfo("property", "p", ContextPropertyType.Property),
        
        new EventPropertyTokenInfo("levelid", "li", EventPropertyType.LevelID),
        new EventPropertyTokenInfo("levelname", "ln", EventPropertyType.LevelName),
        new EventPropertyTokenInfo("levelpriority", "lp", EventPropertyType.LevelPriority),
        //
        new EventPropertyTokenInfo("id", null, EventPropertyType.ID),
        new EventPropertyTokenInfo("category", "cat", EventPropertyType.Category),
        new EventPropertyTokenInfo("code", "cod", EventPropertyType.Code),
        //
        new EventPropertyTokenInfo("year", "yyyy", EventPropertyType.Year),
        new EventPropertyTokenInfo("month", "mm", EventPropertyType.Month),
        new EventPropertyTokenInfo("day", "dd", EventPropertyType.Day),
        new EventPropertyTokenInfo("hour", "h", EventPropertyType.Hour),
        new EventPropertyTokenInfo("minute", "m", EventPropertyType.Minute),
        new EventPropertyTokenInfo("second", "s", EventPropertyType.Second),
        new EventPropertyTokenInfo("millisecond", "ms", EventPropertyType.Millisecond),
        new EventPropertyTokenInfo("date", "dt", EventPropertyType.Date),
        new EventPropertyTokenInfo("time", "tm", EventPropertyType.Time),
        new EventPropertyTokenInfo("timestamp", "ts", EventPropertyType.Timestamp),
        new EventPropertyTokenInfo("zone", "z", EventPropertyType.TimeZone),
        new EventPropertyTokenInfo("monthname", "mn", EventPropertyType.MonthName),
        new EventPropertyTokenInfo("monthshortname", "msn", EventPropertyType.MonthShortName),
        new EventPropertyTokenInfo("weekday", "wd", EventPropertyType.Weekday),
        new EventPropertyTokenInfo("weekdayname", "wdn", EventPropertyType.WeekdayName),
        new EventPropertyTokenInfo("weekdayshortname", "wdsn", EventPropertyType.WeekdayShortName),
        new EventPropertyTokenInfo("dayofyear", "doy", EventPropertyType.DayOfYear),
        new EventPropertyTokenInfo("weekofmonth", "wom", EventPropertyType.WeekOfMonth),
        new EventPropertyTokenInfo("weekofyear", "woy", EventPropertyType.WeekOfYear),
        
        new EventTokenInfo("event", null),

        new MessageTokenInfo("message", "msg"),

        new DataTokenInfo("data", null)
    };
    private static Hashtable tokenInfoMap                   = new Hashtable(111);
    private static Hashtable formattingHintInfoMap          = new Hashtable(111);
    
    static PatternParser() {
        TokenInfo ti;
        for(int x = 0; x < tokenInfoArray.Length; x++) {
            ti = tokenInfoArray[x];
            tokenInfoMap.Add(ti.Name, ti);
            if (ti.AlternateName != null) {
                tokenInfoMap.Add(ti.AlternateName, ti);
            }
        }

        FormattingHintInfo fhi = new MaximumWidthHintInfo("maxwidth", "l");
        formattingHintInfoMap.Add(fhi.Name, fhi);
        formattingHintInfoMap.Add(fhi.Suffix, fhi);        
        fhi = new MinimumWidthHintInfo("minwidth", "s");
        formattingHintInfoMap.Add(fhi.Name, fhi);
        formattingHintInfoMap.Add(fhi.Suffix, fhi);        
        fhi = new WidthHintInfo("width", "w");
        formattingHintInfoMap.Add(fhi.Name, fhi);
        formattingHintInfoMap.Add(fhi.Suffix, fhi);        
        
        AlignmentHintInfo ahi = new AlignmentHintInfo("align", "a");
        ahi.AddAlignment("c", Alignment.Center);
        ahi.AddAlignment("center", Alignment.Center);
        ahi.AddAlignment("l", Alignment.Left);
        ahi.AddAlignment("left", Alignment.Left);
        ahi.AddAlignment("r", Alignment.Right);
        ahi.AddAlignment("right", Alignment.Right);
        formattingHintInfoMap.Add(ahi.Name, ahi);
        formattingHintInfoMap.Add(ahi.Suffix, ahi);        
        
        CaseHintInfo chi = new CaseHintInfo("case", "c");
        chi.AddCase("l", Case.Lower);
        chi.AddCase("lower", Case.Lower);
        chi.AddCase("u", Case.Upper);
        chi.AddCase("upper", Case.Upper);
        formattingHintInfoMap.Add(chi.Name, chi);
        formattingHintInfoMap.Add(chi.Suffix, chi);        
    }
    
    private bool isContextPropertyEnabled;
    private bool isEventPropertyEnabled;
    private bool isEventEnabled;
    private bool isDataEnabled;
    private bool isMessageEnabled;
    private char[] separators;
    
    public PatternParser() : this(true, true, true, true, true, null) {}
    
    public PatternParser(bool _isContextPropertyEnabled, bool _isEventPropertyEnabled, bool _isEventEnabled, bool _isMessageEnabled, bool _isDataEnabled) : this(_isContextPropertyEnabled, _isEventPropertyEnabled, _isEventEnabled, _isMessageEnabled, _isDataEnabled, null) {}
    
    public PatternParser(bool _isContextPropertyEnabled, bool _isEventPropertyEnabled, bool _isEventEnabled, bool _isMessageEnabled, bool _isDataEnabled, char[] _separators) {
        isContextPropertyEnabled = _isContextPropertyEnabled;
        isEventPropertyEnabled = _isEventPropertyEnabled;
        isEventEnabled = _isEventEnabled;
        isMessageEnabled = _isMessageEnabled;
        isDataEnabled = _isDataEnabled;
        if ((_separators == null) || (_separators.Length == 0)) {
            separators = EMPTY_CHAR_ARRAY;
        }
        else {
            separators = new char[_separators.Length];
            Array.Copy(_separators, 0, separators, 0, separators.Length);
        }
    }
    
    public bool IsContextPropertyEnabled {
        get {
            return isContextPropertyEnabled;
        }
        set {
            isContextPropertyEnabled = value;
        }
    }

    public bool IsEventEnabled {
        get {
            return isEventEnabled;
        }
        set {
            isEventEnabled = value;
        }
    }
    
    public bool IsEventPropertyEnabled {
        get {
            return isEventPropertyEnabled;
        }
        set {
            isEventPropertyEnabled = value;
        }
    }
    
    public bool IsDataEnabled {
        get {
            return isDataEnabled;
        }
        set {
            isDataEnabled = value;
        }
    }
    
    public Token[] Parse(string text) {
        if (text == null) {
            return (new Token[] { EMPTY_TEXT_TOKEN });
        }
        text = text.Replace(NEW_LINE_CHAR, NEW_LINE);
        int textLength = text.Length;
        if (textLength == 0) {
            return (new Token[] { EMPTY_TEXT_TOKEN });
        }

        ArrayList tokenList = new ArrayList(50);
        Token token;
        int position = 0;
        int tokenEndPosition = 0;
        int tokenStartPosition = text.IndexOf('{', position);
        while (tokenStartPosition >= 0) {
            token = null;
            tokenEndPosition = text.IndexOf('}', tokenStartPosition);
            if (tokenEndPosition < 0) {
                break;
            }
            else {
                token = ParseToken(text.Substring((tokenStartPosition + 1), ((tokenEndPosition - tokenStartPosition) - 1)));
                if (token != null) {
                    if (tokenStartPosition > position) {
                        tokenList.Add(new TextToken(text.Substring(position, (tokenStartPosition - position))));
                    }
                    tokenList.Add(token);
                    position = (tokenEndPosition + 1);
                }
            }
            if (token != null) {
                tokenStartPosition = text.IndexOf('{', tokenEndPosition);
            }
            else if (tokenStartPosition < (text.Length - 1)) {
                tokenStartPosition = text.IndexOf('{', (tokenStartPosition + 1));
            }
        }
        if (position < textLength) {
            tokenList.Add(new TextToken(text.Substring(position, (text.Length - position))));
        }
        if (tokenList.Count == 0) {
            return (new Token[] { EMPTY_TEXT_TOKEN });
        }
        Token[] tokens = new Token[tokenList.Count];
        tokenList.CopyTo(tokens);
        return tokens;
    }

    public Token ParseToken(string text) {
        if ((text == null) || (text.Length == 0)) {
            return null;
        }
        string tokenName = null;
        string parameter = null;
        string hintsText = null;
        //{context.property("one"):18w,ca,uc}        
        int colonPosition = text.LastIndexOf(COLON_CHAR);
        int parenthesisPosition = text.IndexOf(OPEN_PARENTHESIS_CHAR);
        if (parenthesisPosition < 0) {
            if (colonPosition < 0) {
                tokenName = text;
            }
            else {
                if (colonPosition == 0) {
                    return null;
                }
                else {
                    tokenName = text.Substring(0, colonPosition);
                    if (colonPosition < (text.Length - 1)) {
                        hintsText = text.Substring((colonPosition + 1), (text.Length - (colonPosition + 1)));
                    }
                }
            }
        }
        else {
            if (parenthesisPosition == 0) {
                return null;
            }
            else {
                if (colonPosition < 0) {
                    tokenName = text.Substring(0, parenthesisPosition);
                    if (parenthesisPosition < (text.Length - 1)) {
                        int closeParenthesisPosition = text.LastIndexOf(CLOSE_PARENTHESIS_CHAR);
                        if (closeParenthesisPosition > (parenthesisPosition + 1)) {
                            parameter = text.Substring((parenthesisPosition + 1), ((closeParenthesisPosition - parenthesisPosition) - 1));
                        }
                    }
                }
                else if (parenthesisPosition < colonPosition) {
                    string substring = text.Split(COLON_CHAR)[0];
                    tokenName = substring.Substring(0, parenthesisPosition);
                    int closeParenthesisPosition = text.LastIndexOf(CLOSE_PARENTHESIS_CHAR);
                    if (closeParenthesisPosition > (parenthesisPosition + 1)) {
                        parameter = text.Substring((parenthesisPosition + 1), ((closeParenthesisPosition - parenthesisPosition) - 1));
                    }
                    if (colonPosition < (text.Length - 1)) {
                        hintsText = text.Substring((colonPosition + 1), (text.Length - (colonPosition + 1)));
                    }
                }
                else {
                    if (colonPosition == 0) {
                        return null;
                    }
                    else {
                        tokenName = text.Substring(0, colonPosition);
                        if (colonPosition < (text.Length - 1)) {
                            hintsText = text.Substring((colonPosition + 1), (text.Length - (colonPosition + 1)));
                        }
                    }
                }
            }
        }
        
        tokenName = tokenName.Trim().ToLower();
        if (tokenInfoMap.ContainsKey(tokenName)) {
            TokenInfo tokenInfo = (TokenInfo)tokenInfoMap[tokenName];
            switch (tokenInfo.TokenType) {
                case TokenType.ContextProperty     : {
                    if (isContextPropertyEnabled) {
                        FormattingHints hints = ContextPropertyTokenInfo.DefaultFormattingHints;
                        if (hintsText != null) {
                            hints = ParseFormattingHints(hintsText, hints);
                        }
                        ContextPropertyTokenInfo cpti = (ContextPropertyTokenInfo)tokenInfo;
                        if (cpti.PropertyType == ContextPropertyType.Property) {
                            if (parameter == null) {
                                return null;
                            }
                            else {
                                return new ContextPropertyToken(cpti.PropertyType, parameter, '{' + text + '}', hints.MinimumWidth, hints.MaximumWidth, hints.Alignment, hints.Case);
                            }
                        }
                        else {
                            return new ContextPropertyToken(cpti.PropertyType, null, '{' + text + '}', hints.MinimumWidth, hints.MaximumWidth, hints.Alignment, hints.Case);
                        }
                    }
                    else {
                        return null;
                    }
                }
                case TokenType.EventProperty       : {
                    if (isEventPropertyEnabled) {
                        FormattingHints hints = EventPropertyTokenInfo.DefaultFormattingHints;
                        if (hintsText != null) {
                            hints = ParseFormattingHints(hintsText, hints);
                        }
                        EventPropertyTokenInfo epti = (EventPropertyTokenInfo)tokenInfo;
                        return new EventPropertyToken(epti.PropertyType, '{' + text + '}', hints.MinimumWidth, hints.MaximumWidth, hints.Alignment, hints.Case);
                    }
                    else {
                        return null;
                    }
                }
                case TokenType.Event                : {
                    if (isEventEnabled) {
                        return new EventToken('{' + text + '}');
                    }
                    else {
                        return null;
                    }
                }
                case TokenType.Message              : {
                    if (isMessageEnabled) {
                        FormattingHints hints = MessageTokenInfo.DefaultFormattingHints;
                        if (hintsText != null) {
                            hints = ParseFormattingHints(hintsText, hints);
                        }
                        return new MessageToken('{' + text + '}', hints.MinimumWidth, hints.MaximumWidth, hints.Alignment, hints.Case);
                    }
                    else {
                        return null;
                    }
                }
                case TokenType.Data                 : {
                    if (isDataEnabled) {
                        return new DataToken('{' + text + '}');
                    }
                    else {
                        return null;
                    }
                }
            }
            return null;
        }
        else {
            return null;
        }
    }

    private static FormattingHints ParseFormattingHints(string text, FormattingHints defaults) {
        if (text == null) {
            return defaults;
        }
        Alignment a = defaults.Alignment;
        Case c = defaults.Case;
        int maximumWidth = defaults.MaximumWidth;
        int minimumWidth = defaults.MinimumWidth;
        string[] hintsArray = text.Split(FORMATTING_HINT_SEPARATORS);
        string hint, hintName, hintValue;
        string[] hintArray;
        FormattingHintInfo fhi;
        
        for(int x = 0; x < hintsArray.Length; x++) {
            hint = hintsArray[x].Trim();
            hint = hint.ToLower();
            if (hint.IndexOf(EQUALS_SIGN_CHAR) > 0) {
                // we're dealing with the long form
                hintArray = hint.Split(EQUALS_SIGN_CHAR);
                hintName = hintArray[0].Trim();
                hintValue = hintArray[1].Trim();
            }
            else {
                // we're dealing with the short form
                if (hint.Length < 2) {
                    continue;
                }
                hintName = hint.Substring((hint.Length - 1), 1).Trim();
                hintValue = hint.Substring(0, (hint.Length - 1)).Trim();
            }
            fhi = (FormattingHintInfo)formattingHintInfoMap[hintName];
            if (fhi == null) {
                continue;
            }
            switch (fhi.FormattingHintType) {
                case FormattingHintInfo.ALIGNMENT       : {
                    AlignmentHintInfo ahi = (AlignmentHintInfo)fhi;
                    if (ahi.HasMapping(hintValue)) {
                        a = ahi.GetAlignment(hintValue);
                    }
                    break;
                }
                case FormattingHintInfo.CASE            : {
                    CaseHintInfo chi = (CaseHintInfo)fhi;
                    if (chi.HasMapping(hintValue)) {
                        c = chi.GetCase(hintValue);
                    }
                    break;
                }
                case FormattingHintInfo.MAXIMUM_WIDTH   : {
                    try {
                        maximumWidth = Int32.Parse(hintValue);
                    } catch {}
                    break;
                }
                case FormattingHintInfo.MINIMUM_WIDTH   : {
                    try {
                        minimumWidth = Int32.Parse(hintValue);
                    } catch {}
                    break;
                }
                case FormattingHintInfo.WIDTH           : {
                    try {
                        int i = Int32.Parse(hintValue);
                        maximumWidth = i;
                        minimumWidth = i;
                    } catch {}
                    break;
                }
            }
        }
        return new FormattingHints(minimumWidth, maximumWidth, a, c);
    }
    
    private class FormattingHints {
        public readonly int MinimumWidth;
        public readonly int MaximumWidth;
        public readonly Alignment Alignment;
        public readonly Case Case;
        
        private FormattingHints() {}

        public FormattingHints(int minimumWidth, int maximumWidth, Alignment alignment, Case _case) {
            MinimumWidth = Math.Max(minimumWidth, 0);
            MaximumWidth = Math.Max(MinimumWidth, maximumWidth);
            Alignment = alignment;
            Case = _case;
        }
    }
    
    private abstract class FormattingHintInfo {
        public const int ALIGNMENT          = 0;
        public const int CASE               = 1;
        public const int MAXIMUM_WIDTH      = 2;
        public const int MINIMUM_WIDTH      = 3;
        public const int WIDTH              = 4;
        
        public int FormattingHintType;

        public string Name;
        public string Suffix;
    }
    
    private sealed class AlignmentHintInfo : FormattingHintInfo {
        private Hashtable alignmentMap      = new Hashtable(50);
        
        private AlignmentHintInfo() {}
        
        public AlignmentHintInfo(string name, string suffix) {
            FormattingHintType = ALIGNMENT;
            Name = name;
            Suffix = suffix;
        }

        public void AddAlignment(string key, Alignment a) {
            alignmentMap.Add(key, a);
        }
        
        public bool HasMapping(string s) {
            return alignmentMap.ContainsKey(s);
        }

        public Alignment GetAlignment(string s) {
            return (Alignment)alignmentMap[s];
        }
    }
    
    private sealed class CaseHintInfo : FormattingHintInfo {
        private Hashtable caseMap           = new Hashtable(50);
        
        private CaseHintInfo() {}
        
        public CaseHintInfo(string name, string suffix) {
            FormattingHintType = CASE;
            Name = name;
            Suffix = suffix;
        }

        public void AddCase(string key, Case c) {
            caseMap.Add(key, c);
        }
        
        public bool HasMapping(string s) {
            return caseMap.ContainsKey(s);
        }

        public Case GetCase(string s) {
            return (Case)caseMap[s];
        }
    }
    
    private sealed class MaximumWidthHintInfo : FormattingHintInfo {
        private MaximumWidthHintInfo() {}

        public MaximumWidthHintInfo(string name, string suffix) {
            FormattingHintType = MAXIMUM_WIDTH;
            Name = name;
            Suffix = suffix;
        }
    }

    private sealed class MinimumWidthHintInfo : FormattingHintInfo {
        private MinimumWidthHintInfo() {}

        public MinimumWidthHintInfo(string name, string suffix) {
            FormattingHintType = MINIMUM_WIDTH;
            Name = name;
            Suffix = suffix;
        }
    }
    
    private sealed class WidthHintInfo : FormattingHintInfo {
        private WidthHintInfo() {}

        public WidthHintInfo(string name, string suffix) {
            FormattingHintType = WIDTH;
            Name = name;
            Suffix = suffix;
        }
    }
    
    private abstract class TokenInfo {
        public string Name;
        public string AlternateName;
        
        public TokenType TokenType;
    }
    
    private sealed class ContextPropertyTokenInfo : TokenInfo {
        public static readonly FormattingHints DefaultFormattingHints = new FormattingHints(0, Int32.MaxValue, Alignment.Left, Case.Default);
        public ContextPropertyType PropertyType;
        
        private ContextPropertyTokenInfo() {}

        public ContextPropertyTokenInfo(string name, string alternateName, ContextPropertyType propertyType) {
            TokenType = TokenType.ContextProperty;
            Name = name;
            AlternateName = alternateName;
            PropertyType = propertyType;
        }
    }
    
    private sealed class EventPropertyTokenInfo : TokenInfo {
        public static readonly FormattingHints DefaultFormattingHints = new FormattingHints(0, Int32.MaxValue, Alignment.Left, Case.Default);
        public EventPropertyType PropertyType;

        private EventPropertyTokenInfo() {}

        public EventPropertyTokenInfo(string name, string alternateName, EventPropertyType propertyType) {
            TokenType = TokenType.EventProperty;
            Name = name;
            AlternateName = alternateName;
            PropertyType = propertyType;
        }
    }
    
    private sealed class EventTokenInfo : TokenInfo {
        private EventTokenInfo() {}
        
        public EventTokenInfo(string name, string alternateName) {
            TokenType = TokenType.Event;
            Name = name;
            AlternateName = alternateName;
        }
    }
    
    private sealed class MessageTokenInfo : TokenInfo {
        public static readonly FormattingHints DefaultFormattingHints = new FormattingHints(0, Int32.MaxValue, Alignment.Left, Case.Default);
        private MessageTokenInfo() {}

        public MessageTokenInfo(string name, string alternateName) {
            TokenType = TokenType.Message;
            Name = name;
            AlternateName = alternateName;
        }
    }
    
    private sealed class DataTokenInfo : TokenInfo {
        private DataTokenInfo() {}

        public DataTokenInfo(string name, string alternateName) {
            TokenType = TokenType.Data;
            Name = name;
            AlternateName = alternateName;
        }
    }
    
}

}
